Xamarin.Forms Shell Preview
Xamarin.Forms v4.x プレビュー版の内容です。2019-05-14 に公開された v4.x 安定版の内容で書き直したのは Xamarin.Forms Shell になります。 参考ドキュメント
概要
Shell は階層化されたページを持つアプリを楽に作るための仕組み。定義された階層は、ハンバーガーメニューやタブバーなどで表現される。記述を少なくするためのショートカット記法と、愚直な記法とがあり、それぞれちょっと書き方が変わるが実行結果は同じになる。
Shell の構造
Shell には概念の大きさ順に次の 4 つの要素がある。Shell は ContentPage の代わりに使用する (ContentPage のように App.MainPage に Shell のインスタンスを設定する)。
Shell
ShellItem
ShellSection
ShellContent
最も単純な 1 ページだけのアプリの場合、次のような記述になる (コードとスクリーンショットは Issue から引用)。上に表示されるナビゲーションバーは Shell.NavBarVisible="false" で消すことができる。FlyoutBehavior とは、Flyout という UI パーツを使うかどうかの設定。Flyout は日本人には馴染みのない単語だと思うが、調べてみると、カテゴリーを持っていて、かつ別ページへ遷移するための UI らしい。Android だとハンバーガーメニューで表現される。ちなみに、Xamarin.Forms での開発経験がない人のために補足すると、local:HomePage は ContentPage を継承した独自のページであり、これまでの Xamarin.Forms による開発と変わった点はない。
code: xml
xmlns:local="clr-namespace:MyStore"
FlyoutBehavior="Disabled"
x:Class="MyStore.Shell">
<ShellItem>
<ShellSection>
<ShellContent>
<local:HomePage />
</ShellContent>
</ShellSection>
</ShellItem>
</Shell>
https://gyazo.com/40253f24e1e69e10d7c1483db01cad74
2 ページで構成され、画面下部のタブバーでそれらを切り替える場合は次のようになる (コードとスクリーンショットは Issue から引用)。ShellItem 直下に ShellSection を追加することでタブバーが表示されるようになる。
code:xml
xmlns:local="clr-namespace:MyStore"
FlyoutBehavior="Disabled"
x:Class="MyStore.Shell">
<ShellItem>
<ShellSection Title="Home" Icon="home.png">
<ShellContent>
<local:HomePage />
</ShellContent>
</ShellSection>
<ShellSection Title="Notifications" Icon="bell.png">
<ShellContent>
<local:NotificationsPage />
</ShellContent>
</ShellSection>
</ShellItem>
</Shell>
https://gyazo.com/65aa605268fc90d6ccf91cc864ec755b
各タブでさらに上部にメニューを表示する場合は次のように ShellSection 直下に ShellContent を追加する (コードとスクリーンショットは Issue から引用)。
code:xml
xmlns:local="clr-namespace:MyStore"
FlyoutBehavior="Disabled"
x:Class="MyStore.Shell">
<ShellItem>
<ShellSection Title="Home" Icon="home.png">
<ShellContent>
<local:HomePage />
</ShellContent>
</ShellSection>
<ShellSection Title="Notifications" Icon="bell.png">
<ShellContent Title="Recent">
<local:NotificationsPage />
</ShellContent>
<ShellContent Title="Alert Settings">
<local:SettingsPage />
</ShellContent>
</ShellSection>
</ShellItem>
</Shell>
https://gyazo.com/f86e41f82db65345ae96c1ed92e9b5fc
2 ページを Flyout ナビゲーションにする場合は次のようになる (コードとスクリーンショットは Issue から引用)。ShellItem を追加することで Flyout ナビゲーションからそれらを切り替えれるようになる。Shell.FlyoutHeader は画像のヘッダー部分であり、この例では独自の FlyoutHeader ビューを作成して使用している (local:FlyoutHeader がその独自ビュー)。
code:xml
xmlns:local="clr-namespace:MyStore"
x:Class="MyStore.Shell">
<Shell.FlyoutHeader>
<local:FlyoutHeader />
</Shell.FlyoutHeader>
<ShellItem Title="Home" Icon="home.png">
<ShellSection>
<ShellContent>
<local:HomePage />
</ShellContent>
</ShellSection>
</ShellItem>
<ShellItem Title="Notifications" Icon="bell.png">
<ShellSection>
<ShellContent>
<local:NotificationsPage />
</ShellContent>
</ShellSection>
</ShellItem>
</Shell>
https://gyazo.com/c8f69ed9d99f01ce8c86cdb5ea390f07
ここまでで Shell が用意してる構造定義はすべて。ただし、うまいことやってくれるショートカット記法があるので、以下ではそれについて説明する。
最初に挙げた 1 ページからなる場合は次のように書ける (コードは Issue から引用)。この場合、暗黙的な自動変換により local:HomePage は ShellItem でラッピングされる。タイトルとアイコンも local:HomePage から自動的に設定される。
code:xml
xmlns:local="clr-namespace:MyStore"
FlyoutBehavior="Disabled"
x:Class="MyStore.Shell">
<local:HomePage />
</Shell>
タブバーによる 2 ページの切り替えは次のように書ける (コードは Issue から引用)。この場合、各ページは自動的に ShellContent でラッピングされ、さらにそれぞれが ShellSection でラッピングされる。
code:xml
xmlns:local="clr-namespace:MyStore"
FlyoutBehavior="Disabled"
x:Class="MyStore.Shell">
<ShellItem>
<local:HomePage Icon="home.png" />
<local:NotificationsPage Icon="bell.png" />
</ShellItem>
</Shell>
タブの中にさらにメニューを作る場合は次のようになる (コードは Issue から引用)。分けたい場合だけ、明示的に ShellSection を記述してやる必要がある。
code:xml
xmlns:local="clr-namespace:MyStore"
FlyoutBehavior="Disabled"
x:Class="MyStore.Shell">
<ShellItem>
<local:HomePage Icon="home.png" />
<ShellSection Title="Notifications" Icon="bell.png">
<local:NotificationsPage />
<local:SettingsPage />
</ShellSection>
</ShellItem>
</Shell>
Flyout ナビゲーションで 2 ページを分ける場合は次のようになる (コードは Issue から引用)。これは 1 つ目の例にページを追加し、FlyoutBehavior="Disabled による Flyout の無効化を取っ払った形である。
code:xml
xmlns:local="clr-namespace:MyStore"
x:Class="MyStore.Shell">
<Shell.FlyoutHeader>
<local:FlyoutHeader />
</Shell.FlyoutHeader>
<local:HomePage Icon="home.png" />
<local:NotificationsPage Icon="bell.png" />
</Shell>
ここまででショートカット記法についての説明は終わり。
ナビゲーション
これまでの Navigation.PushAsync() によるもの
ContentPage#Navigation.PushAsync() によるページ遷移は、ShellSection 直下に位置するページの切り替えによって行われる。例えば、タブが 2 つあって右側のタブを表示しているときに PushAsync() すると、そのタブが選択されたままタブ内のページが切り替わる。この動きはおそらくほとんどの場合に意図したものではないので、Shell ナビゲーションを使う上では PushAsync などによる従来のナビゲーションは利用するべきではない。
新しい URI によるもの
Shell によって階層化されたページには 次の形式で URI が提供される。
[Shell.RouteScheme]://[Shell.RouteHost]/[Shell]/[ShellItem]/[ShellSection]/[ShellContent]/[NavStack1]/[NavStack2]...
これらが設定されてない場合はランタイムによって自動生成されるが、開発者が明示的に設定することによって変化しない URI になるのでディープリンクとして利用できるようになる (自動生成ではアプリ実行の度に変化する可能性があるらしい)。
ディープリンクとは
URI 指定により、アプリ内の特定のページ/コンテンツへ直接遷移できる技術 (例えば、ウェブブラウザー上で特定のツイートへの URL をタップしたときに、 Twitter アプリでそのツイートを開くとか)。仕様はそれぞれ独自のものだが iOS/Android ともに似たような仕組みを利用できる。大きく分けてクライアントサイドのみで設定が完結するものと、サーバーサイドにも設定することでアプリが一意の URI を利用できるようになるものがある。後者のほうは特定ドメインに設定ファイルを置かなければならないのでセキュリティ的に優れていたり、確認ダイアログを挟まないのでユーザー体験が良かったりする。
Twitter や Facebook は、それぞれ独自の設定ファイルをサーバーサイドに置いておくことでカード表示されたりするらしい。
Custom URL Scheme (iOS/Android)
インストールされてない場合はエラーになってアプリが起動しない。悪意のある開発者がわざと同じ URI を設定してデザインなども真似てしまえば、個人情報を盗むことも出来てしまう。
Universal Links (iOS)/App Links (Android)
アプリがインストールされてる場合はそのままアプリが起動される。インストールされてない場合はその URL に設定されてるコンテンツが表示される。ちゃんとしたアプリならこっちを使うべきだと思う。
Firebase Dynamic Links (iOS/Android)
Google が提供してる Firebase というサービスのひとつ。サービス内で URL や設定ファイルを生成してくれるので楽っぽい。
Shell の URI 設定
Shell では次の形式で URI が構築される。
[Shell.RouteScheme]://[Shell.RouteHost]/[Shell]/[ShellItem]/[ShellSection]/[ShellContent]/[NavStack1]/[NavStack2]...
RouteScheme
http や app などの URI スキーム。自由に設定できるが、とりあえず app にしておけば良さそう
RouteHost
URI のドメイン部分
Route
アプリ URI の主となる部分
上記 3 つを組み合わせてアプリ URI を構築する。例えば app://microsoft.com/newapp のようになる。
この URI で、XAML を使って Route を作る場合は次のようになる (コードは Xamarin Blog から引用)。この場合、 NotificationPage へ遷移するための URI は app://microsoft.com/newapp/a/b/notifications となる。
code:xml
<ShellItem Title="Home" Route="a">
<ShellSection Route="b">
<ShellContent Route="home">
<local:MainPage />
</ShellContent>
<ShellContent Route="notifications">
<local:NotificationsPage />
</ShellContent>
</ShellSection>
</ShellItem>
Shell の構造から外れたページを Shell のナビゲーションシステムに登録する場合は次のように登録する (コードは Xamarin Blog から引用)。
code:csharp
Routing.RegisterRoute("greeting", typeof(GreetingPage));
上記で登録した GreetingPage へ Shell のナビゲーションシステムを使って遷移するためには次のようなコードを実行する (コードは Xamarin Blog から引用)。
code:csharp
await (App.Current.MainPage as Shell).GoToAsync(new ShellNavigationState(new Uri("app://microsoft.com/newapp/greeting?msg=Hello")));
このコードは次のように短く書くことも出来る (コードは Xamarin Blog から引用)。特別な理由がなければこっちの書きかたをするべきだと思う。
code:csharp
await (App.Current.MainPage as Shell).GoToAsync("app:///newapp/greeting?msg=Hello");
GoToAsync を使うことで、Shell の構造を保ちつつ該当する階層に対象のページが表示される。例えば、次のような定義をしておいて、SettingPage を表示したとする。すると Timeline と Setting のタブが下に表示され、Setting タブが選択された状態になる。その上で await (App.Current.MainPage as Shell).GoToAsync("app:///example.com/main/user/timeline"); を実行すると Timeline タブが選択されて TimelinePage が表示される。
code:xml
<?xml version="1.0" encoding="UTF-8"?>
xmlns:local="clr-namespace:example.Views"
RouteHost="example.com"
RouteScheme="app"
Route="example.com"
FlyoutBehavior="Flyout"
Title="Example"
x:Class="example.AppShell">
<ShellItem Route="main">
<ShellSection Route="user">
<ShellContent Route="timeline">
<local:TimelinePage></local:TimelinePage>
</ShellContent>
<ShellContent Route="setting">
<local:SettingPage></local:SettingPage>
</ShellContent>
</ShellSection>
</ShellItem>
QueryProperty
ContentPage か BindingContext でバインドされたビューモデルクラスに QueryProperty 属性を追加することで、次のようにパラメーターを渡すことができる (コードは Xamarin Blog から引用)。属性にはプロパティー名と、パラメーター名を渡す必要がある (パラメーター名は引数名のようなものなので何でも良い)。
code:csharp
namespace NewApp.Pages
{
public partial class GreetingPage : ContentPage
{
public GreetingPage()
{
InitializeComponent();
}
string _message;
public string Message
{
get { return _message; }
set
{
_message = value;
}
}
}
}